package sw6.packageHandler;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;

/**
 * This class represents the logic for testing the packagehandler.
 * The purpose of this class is to provide a user interface that
 * makes the user able to pick which version of test applications
 * to install and which to upgrade. Further the interface provides
 * options to remove all test packages and most important: to start
 * the series of tests against the packageHandler implementation.
 * <br/>
 * The tests are carried out in the following way:
 * <ol>
 * <li>sw6.appXvY packages are written. The package must contain a
 * settings.xml file, and the package should test some sort of
 * logic relevant to what has been carried out by the packagehandler
 * at the point where the package was installed / upgraded. It
 * is important that the testActivity of the test package is marked
 * as exported in its manifest file. Further, a certain intent-filter
 * must be set up. We suggest to look at the manifest file as well
 * as at the testActivity implementation of a testProject and apply
 * that behavior to other test packages that are to be written.</li>
 * <li>The test package must be copied to the /assets/apps folder of 
 * this project.</li>
 * <li>Run this project.</li>
 * </ol>
 * @author sw6b
 *
 */
public class PackageHandlerTest extends Activity implements OnClickListener {

	// Definitions
	private final static int REQUEST_CODE	= 1337;
	private final static String APP_NAME 	= "sw6.packageHandler";
	
	// Is relative to /assets
	private final static String APK_LOC		= "apps/";

	// Gui elements
	private Button button1 					= null;
	private Button button2 					= null;
	private Button button3 					= null;
	private Button button4 					= null;
	private Button button5 					= null;
	private TextView textView2				= null;
	private TableLayout tableLayout2 		= null;

	/**
	 * Creates the GUI for this package handler test
	 */
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		button1 = (Button) findViewById(R.id.installPackages);
		button2 = (Button) findViewById(R.id.testInstallation);
		button3 = (Button) findViewById(R.id.incrementVersion);
		button4 = (Button) findViewById(R.id.decrementVersion);
		button5 = (Button) findViewById(R.id.removeTestPackages);

		button1.setOnClickListener(this);
		button2.setOnClickListener(this);
		button3.setOnClickListener(this);
		button4.setOnClickListener(this);
		button5.setOnClickListener(this);

		tableLayout2	= (TableLayout) findViewById(R.id.tableLayout2);
		textView2 		= (TextView) findViewById(R.id.version);

		appendNewMessage("PACKAGE-HANDLER TEST CREATED", Color.MAGENTA);
	}

	/**
	 * Appends a new message to the log output. The color of this message
	 * will be set to <code>Color.WHITE</code>.
	 * @param msg A log message.
	 */
	private void appendNewMessage(String msg) {
		appendNewMessage(msg, Color.WHITE);
	}

	/**
	 * Appends a new message to the log output. The color of this message
	 * must be defined using the <code>android.graphics.Color</code> class'
	 * static color definitions.
	 * @param msg A log message.
	 * @param color Some color value defined using statics in 
	 * <code>android.graphics.Color</code>.
	 */
	private void appendNewMessage(String msg, int color) {
		TableRow tr = new TableRow(this);
		TextView tv = new TextView(this);
		tv.setText("> " + msg);
		tv.setTextColor(color);
		tr.addView(tv);
		tableLayout2.addView(tr);
	}
	
	/**
	 * Installs/upgrades packages of the selected version.
	 * This method utilizes the package manager of Android.
	 * Due to security issues, the user is required to
	 * follow the installation on screen. That is, the user
	 * must click the required apply/continue buttons on
	 * the phone when asked to.
	 * @param version Version of the testpackages to install.
	 */
	private void installPackages(int version) {

		// Get the assets manager
		AssetManager am = getAssetManager(this);

		// For every app, install it
		for(String appName : getListOfTestApkFileNames(version)) {

			// Use a tempfile to represent the apk file loaded from the /assets/apps folder
			File tempFile = null;
			try {
				tempFile = File.createTempFile(appName, "tmp");
			} catch (IOException e) {
				Log.e(APP_NAME, "Could not create a temp file on the SD card. " +
						"Check if: android.permission.WRITE_EXTERNAL_STORAGE is in the manifest file.");
				e.printStackTrace();
			}

			// Use an outputstream to write to the tempfile 
			OutputStream os = null;
			try {
				os = new FileOutputStream(tempFile);
			} catch (FileNotFoundException e) {
				Log.e(APP_NAME, "Could not find the temp file on the SD card (the one I am trying to hook up to)");
				e.printStackTrace();
			}

			// Use inputstream to read the binary stream of an apk file
			InputStream is = null;
			try {
				is = am.open(APK_LOC + appName);
			} catch (IOException e) {
				Log.e(APP_NAME, "Could not create inputstream. Check if the apk file: " + appName + ", is still available" +
						"in the /assets/apps folder of: " + APP_NAME);
				e.printStackTrace();
			}

			// As the BufferedInputStream is _very_ slow, we read the apk file "manually"
			byte buf[] = new byte[1024];
			int len = 0;
			try {
				while((len=is.read(buf))>0) {
					os.write(buf,0,len);
				}
			} catch (IOException e) {
				Log.e(APP_NAME, "An exception occured during read of the apk file: " + appName);
				e.printStackTrace();
			}
			
			
			// Try to close the outstream
			try {
				os.close();
			} catch (IOException e) {
				Log.e(APP_NAME, "Could not close the outputstream.");
				e.printStackTrace();
			}
			
			// Try to close the inputstream
			try {
				is.close();
			} catch (IOException e) {
				Log.e(APP_NAME, "Could not close the inputstream.");
				e.printStackTrace();
			}

			// Get filename of the apkfile
			String fileName = tempFile.getPath();
			
			// Start installation through this intent
			Intent intent = new Intent(Intent.ACTION_VIEW);
			// intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);          
			intent.setDataAndType(Uri.fromFile(new File(fileName)), "application/vnd.android.package-archive");
			startActivity(intent); 
		}
	}

	/**
	 * Get the version number to consider (the one chosen
	 * in the GUI)
	 * @return The selected version number in the GUI.
	 */
	private int getVersionUnderTest() {
		return Integer.parseInt(textView2.getText().toString());
	}

	/**
	 * Run all tests for the test packages installed on the phone.
	 */
	private void runTests() {
		String gmtString = Calendar.getInstance().getTime().toLocaleString();
		appendNewMessage("TEST STARTED @ " + gmtString, Color.CYAN);
		
		// For every testpackage installed on the phone, run its tests
		for(String testPackageName : getListOfPackageNamesForTestAppsInstalled()) {
			Intent intent 		= new Intent();
			String className	= testPackageName + ".TestActivity";
			intent.setComponent(new ComponentName(testPackageName, className));
			startActivityForResult(intent, REQUEST_CODE);
		}
	}

	/**
	 * Call-back method used to view the test results returned
	 * by a test package.
	 */
	protected void onActivityResult (int requestCode, int resultCode, Intent data) {

		// We got a request code from one of our testapps
		if(requestCode == REQUEST_CODE) {

			// Get the testresults
			boolean[] results = data.getBooleanArrayExtra("results");

			// Get the name of the app under test and print it
			String appName = data.getStringExtra("appName");
			appendNewMessage("===" + appName + "===");

			// Check the results
			for(int i = 0; i < results.length; i++) {
				if(results[i] == true) {
					String resultMsg = "TEST: " + i + ", RESULT: PASS";
					appendNewMessage(resultMsg, Color.GREEN);

				} else {
					String resultMsg = "TEST: " + i + ", RESULT: FAIL";
					appendNewMessage(resultMsg, Color.RED);
				}
			}
		}
	}

	/**
	 * Gets the <code>AssetManager</code> for a particular
	 * application.
	 * @param context The context which <code>AssetManager</code> one
	 * wants to get.
	 * @return An <code>AssetManager</code> of the context.
	 */
	private AssetManager getAssetManager(Context context) {
		
		// Get instance of package manager
		PackageManager pm 	= context.getPackageManager();
		Resources resc 		= null;
		try {
			resc 			= pm.getResourcesForApplication(context.getPackageName());
		} catch (NameNotFoundException e) {
			Log.e(APP_NAME, "Could not find ressources for: " + APP_NAME + ". I was trying to get" +
					"the ressources, as this is needed to gain access to the assets folder.");
			e.printStackTrace();
		}
		return resc.getAssets();
	}

	/**
	 * Remove all test-packages from the phone.
	 * Due to security, this requires input from the user.
	 */
	private void removeTestApps() {

		// Use the packagemanager to remove all test apps
		for(String assetApkFileName : getListOfPackageNamesForTestAppsInstalled()) {
			Uri packageURI 			= Uri.parse("package:" + assetApkFileName);
			Intent uninstallIntent 	= new Intent(Intent.ACTION_DELETE, packageURI);
			startActivity(uninstallIntent);
		}
	}

	/**
	 * Returns a list of package names of the test apps installed
	 * on the phone. To ensure that only test apps affiliated to 
	 * this testproject is returned, every packagename is matched 
	 * against a regular expression.
	 * @return A list of package names of the test apps installed.
	 */
	private ArrayList<String> getListOfPackageNamesForTestAppsInstalled() {

		ArrayList<String> installedApps	= new ArrayList<String>();
		PackageManager pm 				= this.getPackageManager();
		Intent mainIntent 				= new Intent(Intent.ACTION_MAIN, null);
		
		/* 
		 * To locate the test apps, they all have an intent-filter
		 * saying that they are a part of the DEFAULT CATEGORY.
		 * In this way, we can query for the testapps and know
		 * for sure, that they are returned in the list.
		 */
		mainIntent.addCategory(Intent.CATEGORY_DEFAULT);
		List<ResolveInfo> pkgAppsList 	= pm.queryIntentActivities( mainIntent, 0);

		// Pattern for a test package
		String pattern = "sw6\\.app[0-9]{1}";

		// For all packages that matches the pattern, store them in a list
		for(int i = 0; i < pkgAppsList.size(); i++) {
			String appName = pkgAppsList.get(i).activityInfo.applicationInfo.packageName;
			if(appName.matches(pattern)) {
				installedApps.add(appName);
			}
		}

		// Reverse the order to make the output more "readable"
		Collections.sort(installedApps);
		Collections.reverse(installedApps);
		
		return installedApps;
	}

	/**
	 * Gets a list of test application's file names having 
	 * a specified version.
	 * @param version Version of the test apps to return.
	 * @return A list of test application's file names.
	 */
	private ArrayList<String> getListOfTestApkFileNames(int version) {

		// Gets the list of apk's in /assets/apps
		String[] assetApps = getListOfFileNamesInAssetsApps();

		// Filter to selected version
		ArrayList<String> filteredList = new ArrayList<String>();
		
		// Pattern to match against
		String pattern = "sw6\\.app[0-9]{1}v[" + version + "]{1}";
		
		// Check if the file found matches the desired pattern
		for(String appName : assetApps) {
			if(appName.replace(".apk", "").matches(pattern)) {
				filteredList.add(appName);
			}
		}

		return filteredList;
	}
	
	/**
	 * Returns the list of file names for all the 
	 * apk test files located in the /assets/apps folder
	 * of this application.
	 * @return A list of file names for the test apk's 
	 * located in the /assets/apps folder.
	 */
	private String[] getListOfFileNamesInAssetsApps() {
		
		// Get asset manager
		AssetManager assetManager	= getAssetManager(this);
		String[] assetApps 			= null;
		
		// Get a list of files from /assets/apps
		try {
			assetApps = assetManager.list("apps");
		} catch (IOException e) {
			Log.e(APP_NAME, "Could not retrieve the list of filenames in /assets/apps");
			e.printStackTrace();
		}

		return assetApps;
	}

	/**
	 * Increment the version of apps under test.
	 */
	private void incrementVersion() {
		int incrementedVersion = getVersionUnderTest() + 1;
		textView2.setText(Integer.toString(incrementedVersion));
	}

	/**
	 * Decrement the version of apps under test.
	 */
	private void decrementVersion() {
		int decrementedVersion = getVersionUnderTest() - 1;

		// Avoid illegal version numbers
		if(decrementedVersion > 0) {
			textView2.setText(Integer.toString(decrementedVersion));
		}
	}

	/**
	 * Handles button clicks
	 */
	@Override
	public void onClick(View v) {
		switch(v.getId()) {
		case R.id.installPackages : 
			installPackages(getVersionUnderTest());
			break;
		case R.id.testInstallation :
			runTests();
			break;
		case R.id.incrementVersion :
			incrementVersion();
			break;
		case R.id.decrementVersion :
			decrementVersion();
			break;
		case R.id.removeTestPackages :
			removeTestApps();
			break;
		}
	}
}
